/**
 * Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
 *
 * You are hereby granted a non-exclusive, worldwide, royalty-free license to use,
 * copy, modify, and distribute this software in source code or binary form for use
 * in connection with the web services and APIs provided by Facebook.
 *
 * As with any software that integrates with the Facebook platform, your use of
 * this software is subject to the Facebook Developer Principles and Policies
 * [http://developers.facebook.com/policy/]. This copyright notice shall be
 * included in all copies or substantial portions of the software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

package com.facebook.share.widget;

import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Bundle;
import androidx.fragment.app.Fragment;
import com.facebook.AccessToken;
import com.facebook.FacebookCallback;
import com.facebook.appevents.InternalAppEventsLogger;
import com.facebook.internal.AnalyticsEvents;
import com.facebook.internal.AppCall;
import com.facebook.internal.CallbackManagerImpl;
import com.facebook.internal.DialogFeature;
import com.facebook.internal.DialogPresenter;
import com.facebook.internal.FacebookDialogBase;
import com.facebook.internal.FragmentWrapper;
import com.facebook.internal.NativeAppCallAttachmentStore;
import com.facebook.internal.Utility;
import com.facebook.share.Sharer;
import com.facebook.share.internal.CameraEffectFeature;
import com.facebook.share.internal.LegacyNativeDialogParameters;
import com.facebook.share.internal.NativeDialogParameters;
import com.facebook.share.internal.OpenGraphActionDialogFeature;
import com.facebook.share.internal.ShareContentValidation;
import com.facebook.share.internal.ShareDialogFeature;
import com.facebook.share.internal.ShareFeedContent;
import com.facebook.share.internal.ShareInternalUtility;
import com.facebook.share.internal.ShareStoryFeature;
import com.facebook.share.internal.WebDialogParameters;
import com.facebook.share.model.ShareCameraEffectContent;
import com.facebook.share.model.ShareContent;
import com.facebook.share.model.ShareLinkContent;
import com.facebook.share.model.ShareMediaContent;
import com.facebook.share.model.ShareOpenGraphContent;
import com.facebook.share.model.SharePhoto;
import com.facebook.share.model.SharePhotoContent;
import com.facebook.share.model.ShareStoryContent;
import com.facebook.share.model.ShareVideoContent;

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

/**
 * Provides functionality to share content via the Facebook Share Dialog
 */
public final class ShareDialog
    extends FacebookDialogBase<ShareContent, Sharer.Result>
    implements Sharer {

    /**
     * The mode for the share dialog.
     */
    public enum Mode {
        /**
         * The mode is determined automatically.
         */
        AUTOMATIC,
        /**
         * The native dialog is used.
         */
        NATIVE,
        /**
         * The web dialog is used.
         */
        WEB,
        /**
         * The feed dialog is used.
         */
        FEED
    }

    private static final String TAG = ShareDialog.class.getSimpleName();

    private static final String FEED_DIALOG = "feed";
    public static final String WEB_SHARE_DIALOG = "share";
    private static final String WEB_OG_SHARE_DIALOG = "share_open_graph";

    private static final int DEFAULT_REQUEST_CODE =
        CallbackManagerImpl.RequestCodeOffset.Share.toRequestCode();

    private  boolean shouldFailOnDataError = false;
    // Keep track of Mode overrides for logging purposes.
    private boolean isAutomaticMode = true;

    /**
     * Helper to show the provided {@link com.facebook.share.model.ShareContent} using the provided
     * Activity. No callback will be invoked.
     *
     * @param activity Activity to use to share the provided content
     * @param shareContent Content to share
     */
    public static void show(
        final Activity activity,
        final ShareContent shareContent) {
        new ShareDialog(activity).show(shareContent);
    }

    /**
     * Helper to show the provided {@link com.facebook.share.model.ShareContent} using the provided
     * Fragment. No callback will be invoked.
     *
     * @param fragment androidx.fragment.app.Fragment to use to share the provided content
     * @param shareContent Content to share
     */
    public static void show(
        final Fragment fragment,
        final ShareContent shareContent) {
        show(new FragmentWrapper(fragment), shareContent);
    }

    /**
     * Helper to show the provided {@link com.facebook.share.model.ShareContent} using the provided
     * Fragment. No callback will be invoked.
     *
     * @param fragment android.app.Fragment to use to share the provided content
     * @param shareContent Content to share
     */
    public static void show(
        final android.app.Fragment fragment,
        final ShareContent shareContent) {
        show(new FragmentWrapper(fragment), shareContent);
    }

    private static void show(
        final FragmentWrapper fragmentWrapper,
        final ShareContent shareContent) {
        new ShareDialog(fragmentWrapper).show(shareContent);
    }

    /**
     * Indicates whether it is possible to show the dialog for
     * {@link com.facebook.share.model.ShareContent} of the specified type.
     *
     * @param contentType Class of the intended {@link com.facebook.share.model.ShareContent} to
     *                    share.
     * @return True if the specified content type can be shown via the dialog
     */
    public static boolean canShow(Class<? extends ShareContent> contentType) {
        return canShowWebTypeCheck(contentType) || canShowNative(contentType);
    }

    private static boolean canShowNative(Class<? extends ShareContent> contentType) {
        DialogFeature feature = getFeature(contentType);

        return feature != null && DialogPresenter.canPresentNativeDialogWithFeature(feature);
    }

    private static boolean canShowWebTypeCheck(Class<? extends ShareContent> contentType) {
        // If we don't have an instance of a ShareContent, then all we can do is check whether
        // this is a ShareLinkContent, which can be shared if configured properly.
        // The instance method version of this check is more accurate and should be used on
        // ShareDialog instances.

        // SharePhotoContent currently requires the user staging endpoint, so we need a user access
        // token, so we need to see if we have one

        return ShareLinkContent.class.isAssignableFrom(contentType)
            || ShareOpenGraphContent.class.isAssignableFrom(contentType)
            || (SharePhotoContent.class.isAssignableFrom(contentType)
            && AccessToken.isCurrentAccessTokenActive());
    }

    private static boolean canShowWebCheck(ShareContent content) {
        if (!canShowWebTypeCheck(content.getClass())) {
            return false;
        }

        if (content instanceof ShareOpenGraphContent) {
            final ShareOpenGraphContent ogContent = ((ShareOpenGraphContent) content);
            try {
                ShareInternalUtility.toJSONObjectForWeb(ogContent);
            } catch (Exception e) {
                Utility.logd(
                    TAG,
                    "canShow returned false because the content of the Opem Graph object" +
                        " can't be shared via the web dialog",
                    e);
                return false;
            }
        }
        return true;
    }

    /**
     * Constructs a new ShareDialog.
     * @param activity Activity to use to share the provided content.
     */
    public ShareDialog(Activity activity) {
        super(activity, DEFAULT_REQUEST_CODE);

        ShareInternalUtility.registerStaticShareCallback(DEFAULT_REQUEST_CODE);
    }

    /**
     * Constructs a new ShareDialog.
     * @param fragment androidx.fragment.app.Fragment to use to share the provided content.
     */
    public ShareDialog(Fragment fragment) {
        this(new FragmentWrapper(fragment));

    }

    /**
     * Constructs a new ShareDialog.
     * @param fragment android.app.Fragment to use to share the provided content.
     */
    public ShareDialog(android.app.Fragment fragment) {
        this(new FragmentWrapper(fragment));
    }

    private ShareDialog(FragmentWrapper fragmentWrapper) {
        super(fragmentWrapper, DEFAULT_REQUEST_CODE);

        ShareInternalUtility.registerStaticShareCallback(DEFAULT_REQUEST_CODE);
    }

    // for ShareDialog use only
    ShareDialog(Activity activity, int requestCode) {
        super(activity, requestCode);

        ShareInternalUtility.registerStaticShareCallback(requestCode);
    }

    // for ShareDialog use only
    ShareDialog(Fragment fragment, int requestCode) {
        this(new FragmentWrapper(fragment), requestCode);

    }

    ShareDialog(android.app.Fragment fragment, int requestCode) {
        this(new FragmentWrapper(fragment), requestCode);
    }

    private ShareDialog(FragmentWrapper fragmentWrapper, int requestCode) {
        super(fragmentWrapper, requestCode);

        ShareInternalUtility.registerStaticShareCallback(requestCode);
    }

    @Override
    protected void registerCallbackImpl(
        final CallbackManagerImpl callbackManager,
        final FacebookCallback<Result> callback) {
        ShareInternalUtility.registerSharerCallback(
            getRequestCode(), callbackManager, callback);
    }

    @Override
    public boolean getShouldFailOnDataError() {
        return this.shouldFailOnDataError;
    }

    @Override
    public void setShouldFailOnDataError(boolean shouldFailOnDataError) {
        this.shouldFailOnDataError = shouldFailOnDataError;
    }

    /**
     * Call this to check if the Share Dialog can be shown in a specific mode.
     *
     * @param mode Mode of the Share Dialog
     * @return True if the dialog can be shown in the passed in Mode
     */
    public boolean canShow(ShareContent content, Mode mode) {
        return canShowImpl(content, (mode == Mode.AUTOMATIC) ? BASE_AUTOMATIC_MODE : mode);
    }

    /**
     * Call this to show the Share Dialog in a specific mode
     * @param mode Mode of the Share Dialog
     */
    public void show(ShareContent content, Mode mode) {
        isAutomaticMode = (mode == Mode.AUTOMATIC);

        showImpl(content, isAutomaticMode ? BASE_AUTOMATIC_MODE : mode);
    }

    @Override
    protected AppCall createBaseAppCall() {
        return new AppCall(getRequestCode());
    }

    @Override
    protected List<ModeHandler> getOrderedModeHandlers() {
        ArrayList<ModeHandler> handlers = new ArrayList<>();
        handlers.add(new NativeHandler());
        handlers.add(new FeedHandler()); // Feed takes precedence for link-shares for Mode.AUTOMATIC
        handlers.add(new WebShareHandler());
        handlers.add(new CameraEffectHandler());
        handlers.add(new ShareStoryHandler());//Share into story

        return handlers;
    }

    private class NativeHandler extends ModeHandler {
        @Override
        public Object getMode() {
            return Mode.NATIVE;
        }

        @Override
        public boolean canShow(final ShareContent content, boolean isBestEffort) {
            if (content == null || (content instanceof ShareCameraEffectContent)
                || (content instanceof ShareStoryContent)) {
                return false;
            }

            boolean canShowResult = true;
            if (!isBestEffort) {
                // The following features are considered best-effort and will not prevent the
                // native share dialog from being presented, even if the installed version does
                // not support the feature.
                // However, to let apps pivot to a different approach or dialog (for example, Web),
                // we need to be able to signal back when native support is lacking.
                if (content.getShareHashtag() != null) {
                    canShowResult = DialogPresenter.canPresentNativeDialogWithFeature(
                        ShareDialogFeature.HASHTAG);
                }
                if ((content instanceof ShareLinkContent) &&
                    (!Utility.isNullOrEmpty(((ShareLinkContent)content).getQuote()))) {
                    canShowResult &= DialogPresenter.canPresentNativeDialogWithFeature(
                        ShareDialogFeature.LINK_SHARE_QUOTES);
                }
            }
            return canShowResult && ShareDialog.canShowNative(content.getClass());
        }

        @Override
        public AppCall createAppCall(final ShareContent content) {
            logDialogShare(getActivityContext(), content, Mode.NATIVE);

            ShareContentValidation.validateForNativeShare(content);

            final AppCall appCall = createBaseAppCall();
            final boolean shouldFailOnDataError = getShouldFailOnDataError();

            DialogPresenter.setupAppCallForNativeDialog(
                appCall,
                new DialogPresenter.ParameterProvider() {
                    @Override
                    public Bundle getParameters() {
                        return NativeDialogParameters.create(
                            appCall.getCallId(),
                            content,
                            shouldFailOnDataError);
                    }

                    @Override
                    public Bundle getLegacyParameters() {
                        return LegacyNativeDialogParameters.create(
                            appCall.getCallId(),
                            content,
                            shouldFailOnDataError);
                    }
                },
                getFeature(content.getClass()));

            return appCall;
        }
    }

    private class WebShareHandler extends ModeHandler {
        @Override
        public Object getMode() {
            return Mode.WEB;
        }

        @Override
        public boolean canShow(final ShareContent content, boolean isBestEffort) {
            return (content != null) && ShareDialog.canShowWebCheck(content);
        }

        @Override
        public AppCall createAppCall(final ShareContent content) {
            logDialogShare(getActivityContext(), content, Mode.WEB);

            final AppCall appCall = createBaseAppCall();

            ShareContentValidation.validateForWebShare(content);

            Bundle params;
            if (content instanceof ShareLinkContent) {
                params = WebDialogParameters.create((ShareLinkContent)content);
            } else if (content instanceof SharePhotoContent) {
                final SharePhotoContent photoContent =
                    createAndMapAttachments((SharePhotoContent)content, appCall.getCallId());
                params = WebDialogParameters.create(photoContent);
            } else {
                params = WebDialogParameters.create((ShareOpenGraphContent)content);
            }

            DialogPresenter.setupAppCallForWebDialog(
                appCall,
                getActionName(content),
                params);

            return appCall;
        }

        private String getActionName(ShareContent shareContent) {
            if (shareContent instanceof ShareLinkContent
                || shareContent instanceof SharePhotoContent) {
                return WEB_SHARE_DIALOG;
            } else if (shareContent instanceof ShareOpenGraphContent) {
                return WEB_OG_SHARE_DIALOG;
            }

            return null;
        }

        private SharePhotoContent createAndMapAttachments(
            final SharePhotoContent content,
            final UUID callId) {
            final SharePhotoContent.Builder contentBuilder =
                new SharePhotoContent.Builder().readFrom(content);
            final List<SharePhoto> photos = new ArrayList<>();
            final List<NativeAppCallAttachmentStore.Attachment> attachments = new ArrayList<>();
            for (int i = 0; i < content.getPhotos().size(); i++) {
                SharePhoto sharePhoto = content.getPhotos().get(i);
                final Bitmap photoBitmap = sharePhoto.getBitmap();

                if (photoBitmap != null) {
                    NativeAppCallAttachmentStore.Attachment attachment =
                        NativeAppCallAttachmentStore.createAttachment(callId, photoBitmap);
                    sharePhoto = new SharePhoto.Builder()
                        .readFrom(sharePhoto)
                        .setImageUrl(Uri.parse(attachment.getAttachmentUrl()))
                        .setBitmap(null)
                        .build();
                    attachments.add(attachment);
                }

                photos.add(sharePhoto);
            }
            contentBuilder.setPhotos(photos);
            NativeAppCallAttachmentStore.addAttachments(attachments);
            return contentBuilder.build();
        }
    }

    private class FeedHandler extends ModeHandler {
        @Override
        public Object getMode() {
            return Mode.FEED;
        }

        @Override
        public boolean canShow(final ShareContent content, boolean isBestEffort) {
            return (content instanceof ShareLinkContent)
                || (content instanceof ShareFeedContent);
        }

        @Override
        public AppCall createAppCall(final ShareContent content) {
            logDialogShare(getActivityContext(), content, Mode.FEED);
            AppCall appCall = createBaseAppCall();
            Bundle params;
            if (content instanceof ShareLinkContent) {
                ShareLinkContent linkContent = (ShareLinkContent)content;
                ShareContentValidation.validateForWebShare(linkContent);
                params = WebDialogParameters.createForFeed(linkContent);
            } else {
                ShareFeedContent feedContent = (ShareFeedContent)content;
                params = WebDialogParameters.createForFeed(feedContent);
            }

            DialogPresenter.setupAppCallForWebDialog(
                appCall,
                FEED_DIALOG,
                params);

            return appCall;
        }
    }

    private class CameraEffectHandler extends ModeHandler {
        @Override
        public Object getMode() {
            return Mode.NATIVE;
        }

        @Override
        public boolean canShow(final ShareContent content, boolean isBestEffort) {
            boolean canShowResult = (content instanceof ShareCameraEffectContent);

            return canShowResult && ShareDialog.canShowNative(content.getClass());
        }

        @Override
        public AppCall createAppCall(final ShareContent content) {
            ShareContentValidation.validateForNativeShare(content);

            final AppCall appCall = createBaseAppCall();
            final boolean shouldFailOnDataError = getShouldFailOnDataError();

            DialogPresenter.setupAppCallForNativeDialog(
                appCall,
                new DialogPresenter.ParameterProvider() {
                    @Override
                    public Bundle getParameters() {
                        return NativeDialogParameters.create(
                            appCall.getCallId(),
                            content,
                            shouldFailOnDataError);
                    }

                    @Override
                    public Bundle getLegacyParameters() {
                        return LegacyNativeDialogParameters.create(
                            appCall.getCallId(),
                            content,
                            shouldFailOnDataError);
                    }
                },
                getFeature(content.getClass()));

            return appCall;
        }
    }

    private class ShareStoryHandler extends ModeHandler {
        @Override
        public Object getMode() {
            return Mode.NATIVE;
        }

        @Override
        public boolean canShow(final ShareContent content, boolean isBestEffort) {
            boolean canShowResult = (content instanceof ShareStoryContent);

            return canShowResult && ShareDialog.canShowNative(content.getClass());
        }

        @Override
        public AppCall createAppCall(final ShareContent content) {
            ShareContentValidation.validateForStoryShare(content);

            final AppCall appCall = createBaseAppCall();
            final boolean shouldFailOnDataError = getShouldFailOnDataError();

            DialogPresenter.setupAppCallForNativeDialog(
                appCall,
                new DialogPresenter.ParameterProvider() {
                    @Override
                    public Bundle getParameters() {
                        return NativeDialogParameters.create(
                            appCall.getCallId(),
                            content,
                            shouldFailOnDataError);
                    }

                    @Override
                    public Bundle getLegacyParameters() {
                        return LegacyNativeDialogParameters.create(
                            appCall.getCallId(),
                            content,
                            shouldFailOnDataError);
                    }
                },
                getFeature(content.getClass()));

            return appCall;
        }
    }

    private static DialogFeature getFeature(
        Class<? extends ShareContent> contentType) {
        if (ShareLinkContent.class.isAssignableFrom(contentType)) {
            return ShareDialogFeature.SHARE_DIALOG;
        } else if (SharePhotoContent.class.isAssignableFrom(contentType)) {
            return ShareDialogFeature.PHOTOS;
        } else if (ShareVideoContent.class.isAssignableFrom(contentType)) {
            return ShareDialogFeature.VIDEO;
        } else if (ShareOpenGraphContent.class.isAssignableFrom(contentType)) {
            return OpenGraphActionDialogFeature.OG_ACTION_DIALOG;
        } else if (ShareMediaContent.class.isAssignableFrom(contentType)) {
            return ShareDialogFeature.MULTIMEDIA;
        } else if (ShareCameraEffectContent.class.isAssignableFrom(contentType)) {
            return CameraEffectFeature.SHARE_CAMERA_EFFECT;
        } else if (ShareStoryContent.class.isAssignableFrom(contentType)){
            return ShareStoryFeature.SHARE_STORY_ASSET;
        }
        return null;
    }

    private void logDialogShare(Context context, ShareContent content, Mode mode) {
        String displayType;
        if (isAutomaticMode) {
            mode = Mode.AUTOMATIC;
        }

        switch (mode) {
            case AUTOMATIC:
                displayType = AnalyticsEvents.PARAMETER_SHARE_DIALOG_SHOW_AUTOMATIC;
                break;
            case WEB:
                displayType = AnalyticsEvents.PARAMETER_SHARE_DIALOG_SHOW_WEB;
                break;
            case NATIVE:
                displayType = AnalyticsEvents.PARAMETER_SHARE_DIALOG_SHOW_NATIVE;
                break;
            default:
                displayType = AnalyticsEvents.PARAMETER_SHARE_DIALOG_SHOW_UNKNOWN;
                break;
        }

        String contentType;
        DialogFeature dialogFeature = getFeature(content.getClass());
        if (dialogFeature == ShareDialogFeature.SHARE_DIALOG) {
            contentType = AnalyticsEvents.PARAMETER_SHARE_DIALOG_CONTENT_STATUS;
        } else if (dialogFeature == ShareDialogFeature.PHOTOS) {
            contentType = AnalyticsEvents.PARAMETER_SHARE_DIALOG_CONTENT_PHOTO;
        } else if (dialogFeature == ShareDialogFeature.VIDEO) {
            contentType = AnalyticsEvents.PARAMETER_SHARE_DIALOG_CONTENT_VIDEO;
        } else if (dialogFeature == OpenGraphActionDialogFeature.OG_ACTION_DIALOG) {
            contentType = AnalyticsEvents.PARAMETER_SHARE_DIALOG_CONTENT_OPENGRAPH;
        } else {
            contentType = AnalyticsEvents.PARAMETER_SHARE_DIALOG_CONTENT_UNKNOWN;
        }

        InternalAppEventsLogger logger = new InternalAppEventsLogger(context);
        Bundle parameters = new Bundle();
        parameters.putString(
            AnalyticsEvents.PARAMETER_SHARE_DIALOG_SHOW,
            displayType
        );
        parameters.putString(
            AnalyticsEvents.PARAMETER_SHARE_DIALOG_CONTENT_TYPE,
            contentType
        );
        logger.logEventImplicitly(AnalyticsEvents.EVENT_SHARE_DIALOG_SHOW, parameters);
    }
}
